// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: taekwon

#pragma once

#include <algorithm>
#include <cstdio>
#include <ctime>
#include <iomanip>
#include <memory>
#include <string>

#include <openssl/hmac.h>

#include "coin2/base/base64.h"

namespace impl {

class ApiSignatureUtil {
 public:
  static char Dec2hexChar(int16_t n) {
    if (0 <= n && n <= 9) {
      return static_cast<char>('0' + n);
    } else if (10 <= n && n <= 15) {
      return static_cast<char>('A' + n - 10);
    } else {
      return static_cast<char>(0);
    }
  }

  static std::string EscapeURL(const std::string& url) {
    std::string result;
    for (char c : url) {
      if (('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '/' ||
          c == '.' || c == '-' || c == '_' || c == '#' || c == '?') {
        result += c;
      } else {
        int j = static_cast<int16_t>(c);
        if (j < 0) {
          j += 256;
        }
        int i1;
        int i0;
        i1 = j / 16;
        i0 = j - i1 * 16;
        result += '%';
        result += Dec2hexChar(i1);
        result += Dec2hexChar(i0);
      }
    }
    return result;
  }

  static std::string UrlEncode(const std::string& url) {
    const char* str = url.c_str();
    std::unique_ptr<char[]> encstr(new char[strlen(str) * 3 + 1]);
    char buf[2 + 1];
    unsigned char c;
    int i, j;

    if (str == NULL) return NULL;

    for (i = j = 0; str[i]; i++) {
      c = (unsigned char)str[i];
      if ((c >= '0') && (c <= '9')) {
        encstr[j++] = c;
      } else if ((c >= 'A') && (c <= 'Z')) {
        encstr[j++] = c;
      } else if ((c >= 'a') && (c <= 'z')) {
        encstr[j++] = c;
      } else if (
          (c == '@') || (c == '.') || (c == '=') || (c == '\\') || (c == '-') || (c == '_') ||
          (c == ':') || (c == '&')) {
        encstr[j++] = c;
      } else {
        snprintf(buf, sizeof(buf), "%02X", c);
        encstr[j++] = '%';
        encstr[j++] = buf[0];
        encstr[j++] = buf[1];
      }
    }
    encstr[j] = '\0';

    return std::string(encstr.get());
  }

  static std::string GetSignatureByHmacSHA256(
      const std::string& plain,
      const std::string& secret_key) {
    const EVP_MD* engine = EVP_sha256();
    unsigned char output[SHA256_DIGEST_LENGTH] = {0};
    uint32_t len = SHA256_DIGEST_LENGTH;

    Hmac(plain, secret_key, engine, output, &len);

    std::string code;
    code = base64_encode(output, len);

    return code;
  }

  static std::string GetUrlSafeSignatureByHmacSHA256(
      const std::string& plain,
      const std::string& secret_key) {
    const EVP_MD* engine = EVP_sha256();
    unsigned char output[SHA256_DIGEST_LENGTH] = {0};
    uint32_t len = SHA256_DIGEST_LENGTH;

    Hmac(plain, secret_key, engine, output, &len);

    std::string code;
    code = urlsafe_base64_encode(output, len);

    return code;
  }

  static std::string GetSignatureByHexHmacSHA256(
      const std::string& plain,
      const std::string& secret_key) {
    const EVP_MD* engine = EVP_sha256();
    unsigned char output[SHA256_DIGEST_LENGTH] = {0};
    uint32_t len = SHA256_DIGEST_LENGTH;

    Hmac(plain, secret_key, engine, output, &len);

    std::stringstream ss;
    for (unsigned int i = 0; i < len; i++) {
      ss << std::setw(2) << std::setfill('0') << std::hex << static_cast<int>(output[i]);
    }

    return ss.str();
  }

  static std::string GetSignatureByHexHmacSHA512(
      const std::string& plain,
      const std::string& secret_key) {
    const EVP_MD* engine = EVP_sha512();
    unsigned char output[SHA512_DIGEST_LENGTH] = {0};
    uint32_t len = SHA512_DIGEST_LENGTH;

    Hmac(plain, secret_key, engine, output, &len);

    std::stringstream ss;
    for (unsigned int i = 0; i < len; i++) {
      ss << std::setw(2) << std::setfill('0') << std::hex << static_cast<int>(output[i]);
    }

    return ss.str();
  }

  static std::string GetSignatureByHmacSHA512(
      const std::string& plain,
      const std::string& secret_key) {
    const EVP_MD* engine = EVP_sha512();
    unsigned char output[SHA512_DIGEST_LENGTH] = {0};
    uint32_t len = SHA512_DIGEST_LENGTH;

    Hmac(plain, secret_key, engine, output, &len);

    return base64_encode(output, len);
  }

  static std::string GetHashByHexSHA512(const std::string& query_str) {
    unsigned char md_value[SHA512_DIGEST_LENGTH];
    unsigned int md_len;
    const EVP_MD* md = EVP_sha512();

// openssl 1.0.x
#ifdef OPENSSL_VERSION_1_0
    // TODO(daniel) - didn't test
    EVP_MD_CTX mdctx;
    EVP_MD_CTX_init(&mdctx);
    EVP_DigestInit_ex(&mdctx, md, NULL);
    EVP_DigestUpdate(&mdctx, query_str.data(), query_str.length());
    EVP_DigestFinal_ex(&mdctx, md_value, &md_len);
    EVP_MD_CTX_cleanup(&mdctx);
#endif

// openssl 1.1.x and 3.0.x
#if defined OPENSSL_VERSION_1_1 || defined OPENSSL_VERSION_3_0
    EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
    EVP_DigestInit_ex(mdctx, md, NULL);
    EVP_DigestUpdate(mdctx, query_str.data(), query_str.length());
    EVP_DigestFinal_ex(mdctx, md_value, &md_len);
    EVP_MD_CTX_free(mdctx);
#endif

    std::stringstream ss;
    for (unsigned int i = 0; i < md_len; i++) {
      ss << std::setw(2) << std::setfill('0') << std::hex << static_cast<int>(md_value[i]);
    }

    return ss.str();
  }

  static std::string GetHashByRawSHA256(const std::string& data) {
    unsigned char output[SHA256_DIGEST_LENGTH];

    SHA256(reinterpret_cast<const unsigned char*>(data.c_str()), data.length(), output);

    return std::string(reinterpret_cast<char*>(output), SHA256_DIGEST_LENGTH);
  }

  static std::string GetHashBySHA256(const std::string& data) {
    unsigned char output[SHA256_DIGEST_LENGTH];

    SHA256(reinterpret_cast<const unsigned char*>(data.c_str()), data.length(), output);

    return base64_encode(output, SHA256_DIGEST_LENGTH);
  }

 private:
  static void Hmac(
      const std::string& plain,
      const std::string& secret_key,
      const EVP_MD* engine,
      unsigned char* output,
      uint32_t* len) {
// openssl 1.0.x
#ifdef OPENSSL_VERSION_1_0
    HMAC_CTX ctx;
    HMAC_CTX_init(&ctx);
    HMAC_Init_ex(&ctx, secret_key.c_str(), secret_key.size(), engine, NULL);
    HMAC_Update(&ctx, (unsigned char*)plain.c_str(), plain.size());
    HMAC_Final(&ctx, output, len);
    HMAC_CTX_cleanup(&ctx);
#endif

// openssl 1.1.x
#ifdef OPENSSL_VERSION_1_1
    HMAC_CTX* ctx = HMAC_CTX_new();
    HMAC_Init_ex(ctx, secret_key.c_str(), secret_key.size(), engine, nullptr);
    HMAC_Update(ctx, reinterpret_cast<const unsigned char*>(plain.c_str()), plain.size());
    HMAC_Final(ctx, output, len);
    HMAC_CTX_free(ctx);
#endif

// openssl 3.0.x
#ifdef OPENSSL_VERSION_3_0
    HMAC(
        engine,
        secret_key.c_str(),
        secret_key.size(),
        reinterpret_cast<const unsigned char*>(plain.c_str()),
        plain.size(),
        output,
        len);
#endif
  }
};

}  // namespace impl

using ::impl::ApiSignatureUtil;
